Verifying Wishbone bus behavior using Amaranth - Single Read/Write
As part of a design for another project, I am using a wishbone "bus" (wishbone is a methodology for bus design, not technically a bus standard).
To start I setup a signature for the wishbone interface. I personally like having my read and write port written as port.w.data
and port.r.data
which is why the ports are separate. Wishbone shares other values such as address, stb, and cycle between and write.
class WritePort(wiring.Signature):
def __init__(self, address_shape, data_shape):
super().__init__({
"data": Out(data_shape),
"enable": Out(1)
})
class ReadPort(wiring.Signature):
def __init__(self, address_shape, data_shape):
super().__init__({
"data": In(data_shape)
})
class Bus(wiring.Signature):
def __init__(self, address_shape, data_shape, sel_width = 1, burst = False):
ports = {
"w": Out(WritePort(address_shape, data_shape)),
"r": Out(ReadPort(address_shape, data_shape)),
"addr": Out(address_shape),
"sel": Out(sel_width),
"cycle": Out(1),
"stb": Out(1),
"ack": In(1)
}
if burst:
ports = ports | {"cti": Out(3)}
super().__init__(ports)
Since wishbone offers a few optional signals, those are left as options to add. As I build more elaborate modules, I am going to add some more ports.
Test functions
With that done, I want to create a few functions to make writing tests for wishbone interfaces easy. For a client interface, I want to be able to write data to an address, and then read data from an address
The write function is written to slot nicely into Amaranth's testbench framework. The ctx
object provides useful methods for working with the simulator. port
is the port under test (usually the top level wishbone interface for the DUT).
async def write_single(ctx, port, addr, data):
ctx.set(port.w.data, data)
ctx.set(port.addr, addr)
ctx.set(port.w.enable, 1)
ctx.set(port.stb, 1)
ctx.set(port.cycle, 1)
await ctx.tick().until(port.ack)
ctx.set(port.stb, 0)
ctx.set(port.w.enable, 0)
ctx.set(port.cycle, 0)
The until
method of TickTrigger
(returned by ctx.tick()
) is a really helpful method. The TickTrigger
documentation is helpful for writing concise testbenches. Coming from SystemVerilog, it allows an easy flexible framework for writing tests.
The read function is similar, but I use the sample
method to receive data.
async def read_single(ctx, port, addr, expect):
ctx.set(port.addr, addr)
ctx.set(port.w.enable, 0)
ctx.set(port.cycle, 1)
ctx.set(port.stb, 1)
data, = await ctx.tick().sample(port.r.data).until(port.ack)
assert data == expect
ctx.set(port.stb, 0)
ctx.set(port.cycle, 0)
For now I have the assert in the scope of the function. Some flexibility can be added by returning it instead. For unit tests I find that a simple assert is enough information, since I can then refer to the waveform, which stops right at the error condition. Some more information can be provided using a python unittest framework.
After working on some projects for a bit, I also added a poll function. This is helpful when I want to wait for some register to be ready before checking other values.
async def poll(ctx, port, addr, until):
counter = 0
while await read_single(ctx, port, addr) != until:
counter += 1
return counter
This function waits for a bus read to return a value until
.
To do a testbench, I can use these functions to check my module's functionality:
class Device(wiring.Component):
bus: Bus(32, 32)
def elaborate(self, platform):
m = Module()
# Do work here
return m
dut = Module()
dut.submodules.device = device = Device()
async def wb_testbench(ctx):
# Write data to component at address 10
await write_single(ctx, device.bus, 10, 2)
# Read data from address 10
assert await read_single(ctx, device.bus, 10) == 2
# Wait for some register at address 11 to be set
await poll(ctx, device.bus, 11)
# Check another register at address 15
assert await read_single(ctx, device.bus, 15) == 10
sim = Simulator(dut)
sim.add_clock(1e-8)
sim.add_testbench(wb_testbench)
with sim.write_vcd("bench.vcd"):
sim.run()
This doesnt check for wishbone features and expected failures, but is a fast way to check for functionality. When developing a bus based module system, it greatly speeds up development time to have a flexible testing framework.